package com.dbflow5.processor.definition;

import com.dbflow5.StringUtils;
import com.dbflow5.annotation.OneToMany;
import com.dbflow5.annotation.OneToManyMethod;
import com.dbflow5.processor.ClassNames;
import com.dbflow5.processor.ProcessorManager;
import com.dbflow5.processor.definition.column.ColumnAccessCombiner;
import com.dbflow5.processor.definition.column.ColumnAccessor;
import com.dbflow5.processor.utils.ElementExtensions;
import com.dbflow5.processor.utils.ModelUtils;
import com.dbflow5.processor.utils.ProcessorUtils;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.WildcardTypeName;

import javax.lang.model.element.*;
import javax.lang.model.type.TypeMirror;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * Description: Represents the [OneToMany] annotation.
 */
public class OneToManyDefinition extends BaseDefinition {

    private String _methodName;

    public String variableName;

    public List<OneToManyMethod> methods = new ArrayList<>();

    public boolean isLoad() {
        return isAll() || methods.contains(OneToManyMethod.LOAD);
    }

    public boolean isAll() {
        return methods.contains(OneToManyMethod.ALL);
    }

    public boolean isDelete() {
        return isAll() || methods.contains(OneToManyMethod.DELETE);
    }

    public boolean isSave() {
        return isAll() || methods.contains(OneToManyMethod.SAVE);
    }

    public TypeName referencedTableType = null;
    public boolean hasWrapper = false;

    private ColumnAccessor columnAccessor;
    private boolean extendsModel = false;
    private TypeElement referencedType = null;

    private boolean efficientCodeMethods = false;

    public OneToManyDefinition(ExecutableElement executableElement, ProcessorManager processorManager, List<Element> parentElements){
        super(executableElement, processorManager);
        OneToMany oneToMany = executableElement.getAnnotation(OneToMany.class);

        efficientCodeMethods = oneToMany.efficientMethods();

        _methodName = executableElement.getSimpleName().toString();
        variableName = oneToMany.variableName();
        if (variableName.isEmpty()) {
            variableName = _methodName.replaceFirst("get", "");
            variableName = variableName.substring(0, 1).toLowerCase() + variableName.substring(1);
        }

        ColumnAccessor.PrivateScopeColumnAccessor privateAccessor = new ColumnAccessor.PrivateScopeColumnAccessor(variableName, new ColumnAccessor.GetterSetter() {
            @Override
            public String getterName() {
                return "";
            }

            @Override
            public String setterName() {
                return "set" + StringUtils.capitalize(variableName);
            }
        }, false, hasWrapper? ModelUtils.wrapper : "");

        boolean isVariablePrivate = false;
        Element referencedElement = null;
        if(parentElements != null && parentElements.size() > 0) {
            Element element = parentElements.get(0);
            if(element.getSimpleName() != null && element.getSimpleName().toString().equals(variableName)) {
                referencedElement = element;
            }
        }

        if (referencedElement == null) {
            // check on setter. if setter exists, we can reference it safely since a getter has already been defined.
            if (!parentElements.stream().anyMatch(it -> it.getSimpleName().toString().equals(privateAccessor.setterNameElement()) )) {
                manager.logError(OneToManyDefinition.class,
                "@OneToMany definition $elementName " +
                        "Cannot find setter ${privateAccessor.setterNameElement} " +
                        "for variable $variableName.");
            } else {
                isVariablePrivate = true;
            }
        } else {
            isVariablePrivate = referencedElement.getModifiers().contains(Modifier.PRIVATE);
        }

        methods.addAll(Arrays.asList(oneToMany.oneToManyMethods()));

        List<? extends VariableElement> parameters = executableElement.getParameters();
        if (!parameters.isEmpty()) {
            if (parameters.size() > 1) {
                manager.logError(OneToManyDefinition.class, "OneToMany Methods can only have one parameter and that be the DatabaseWrapper.");
            } else {
                VariableElement param = parameters.get(0);
                TypeName name = TypeName.get(param.asType());
                if (name == com.dbflow5.processor.ClassNames.DATABASE_WRAPPER) {
                    hasWrapper = true;
                } else {
                    manager.logError(OneToManyDefinition.class, "OneToMany Methods can only specify a ${com.dbflow5.processor.ClassNames.DATABASE_WRAPPER} as its parameter.");
                }
            }
        }

        columnAccessor = isVariablePrivate? privateAccessor : new ColumnAccessor.VisibleScopeColumnAccessor(variableName);

        TypeMirror returnType = executableElement.getReturnType();
        TypeName typeName = TypeName.get(returnType);
        if (typeName instanceof ParameterizedTypeName) {
            List<TypeName> typeArguments = ((ParameterizedTypeName) typeName).typeArguments;
            if (typeArguments.size() == 1) {
                TypeName refTableType = typeArguments.get(0);
                if (refTableType instanceof WildcardTypeName) {
                    refTableType = ((WildcardTypeName) refTableType).upperBounds.get(0);
                }
                referencedTableType = refTableType;

                referencedType = ElementExtensions.toTypeElement(referencedTableType, manager);
                extendsModel = ProcessorUtils.isSubclass(referencedType, manager.processingEnvironment, com.dbflow5.processor.ClassNames.MODEL);
            }
        }

        methodName = ModelUtils.variable + "."+_methodName+"("+ ColumnAccessCombiner.wrapperIfBaseModel(hasWrapper)+")";
    }

    private final String methodName;

    /**
     * Writes the method to the specified builder for loading from DB.
     *
     * @param codeBuilder codeBuilder
     */
    public void writeLoad(CodeBlock.Builder codeBuilder) {
        if (isLoad()) {
            codeBuilder.addStatement(methodName);
        }
    }

    /**
     * Writes a delete method that will delete all related objects.
     *
     * @param method method
     */
    public void writeDelete(MethodSpec.Builder method) {
        if (isDelete()) {
            writeLoopWithMethod(method, "delete");
            method.addStatement(columnAccessor.set(CodeBlock.of("null"), ColumnAccessor.modelBlock, false));
        }
    }

    public void writeSave(MethodSpec.Builder codeBuilder) {
        if (isSave()) writeLoopWithMethod(codeBuilder, "save");
    }

    public void writeUpdate(MethodSpec.Builder codeBuilder) {
        if (isSave()) writeLoopWithMethod(codeBuilder, "update");
    }

    public void writeInsert(MethodSpec.Builder codeBuilder) {
        if (isSave()) writeLoopWithMethod(codeBuilder, "insert");
    }

    private void writeLoopWithMethod(MethodSpec.Builder codeBuilder, String methodName) {
        String oneToManyMethodName = this.methodName;
        String statement = oneToManyMethodName + " != null";
        CodeBlock.Builder builder = CodeBlock.builder().beginControlFlow("if" + (com.dbflow5.StringUtils.isNullOrEmpty(statement)? "" : " ("+statement+")"));

        // need to load adapter for non-model classes
        if (!extendsModel || efficientCodeMethods) {
            codeBuilder.addStatement("$T adapter = $T.getModelAdapter($T.class)",
                    ParameterizedTypeName.get(ClassNames.MODEL_ADAPTER, referencedTableType),
                    ClassNames.FLOW_MANAGER, referencedTableType);
        }

        if (efficientCodeMethods) {
            codeBuilder.addStatement("adapter."+methodName+"All("+oneToManyMethodName+""+ColumnAccessCombiner.wrapperCommaIfBaseModel(true)+")");
        } else {
            String forStatement = "$T value: " + oneToManyMethodName;
            CodeBlock.builder().beginControlFlow("for" + (com.dbflow5.StringUtils.isNullOrEmpty(forStatement)? "" : " ("+forStatement+")"), ClassName.get(referencedType));

            if (!extendsModel) {
                codeBuilder.addStatement("adapter."+methodName+"(value"+ColumnAccessCombiner.wrapperCommaIfBaseModel(true)+")");
            } else {
                codeBuilder.addStatement("value."+methodName+"("+ColumnAccessCombiner.wrapperIfBaseModel(true)+")");
            }
        }
        builder.endControlFlow();
    }
}

